home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-orca / orca / liveregions.py < prev    next >
Encoding:
Python Source  |  2009-04-13  |  23.6 KB  |  591 lines

  1. import bisect
  2. import gobject
  3. import orca_state
  4. import pyatspi
  5. import speech
  6. import copy
  7. import time
  8.  
  9. from orca_i18n import _
  10.  
  11. # define 'live' property types
  12. LIVE_OFF       = -1
  13. LIVE_NONE      = 0
  14. LIVE_POLITE    = 1
  15. LIVE_ASSERTIVE = 2
  16. LIVE_RUDE      = 3
  17.  
  18. # Seconds a message is held in the queue before it is discarded
  19. MSG_KEEPALIVE_TIME = 45  # in seconds
  20.  
  21. # The number of messages that are cached and can later be reviewed via 
  22. # LiveRegionManager.reviewLiveAnnouncement.
  23. CACHE_SIZE = 9  # corresponds to one of nine key bindings
  24.  
  25. class PriorityQueue:
  26.     """ This class represents a thread **UNSAFE** priority queue where priority
  27.     is determined by the given integer priority.  The entries are also   
  28.     maintained in chronological order. 
  29.  
  30.     TODO: experiment with Queue.Queue to make thread safe
  31.     """
  32.     def __init__(self):
  33.         self.queue = []
  34.  
  35.     def enqueue(self, data, priority, obj):
  36.         """ Add a new element to the queue according to 1) priority and
  37.         2) timestamp. """
  38.         bisect.insort_left(self.queue, (priority, time.time(), data, obj))
  39.        
  40.     def dequeue(self):
  41.         """get the highest priority element from the queue.  """
  42.         return self.queue.pop(0)
  43.  
  44.     def clear(self):
  45.         """ Clear the queue """
  46.         self.queue = []
  47.  
  48.     def purgeByKeepAlive(self):
  49.         """ Purge items from the queue that are older than the keepalive 
  50.         time """
  51.         currenttime = time.time()
  52.         myfilter = lambda item: item[1] + MSG_KEEPALIVE_TIME > currenttime
  53.         self.queue = filter(myfilter, self.queue)
  54.  
  55.     def purgeByPriority(self, priority):
  56.         """ Purge items from the queue that have a lower than or equal priority
  57.         than the given argument """
  58.         myfilter = lambda item: item[0] > priority
  59.         self.queue = filter(myfilter, self.queue)
  60.  
  61.     def clumpContents(self):
  62.         """ Combines messages with the same 'label' by appending newer  
  63.         'content' and removing the newer message.  This operation is only
  64.         applied to the next dequeued message for performance reasons and is
  65.         often applied in conjunction with filterContents() """
  66.         if len(self.queue):
  67.             newqueue = []
  68.             newqueue.append(self.queue[0])
  69.             targetlabels = newqueue[0][2]['labels']
  70.             targetcontent = newqueue[0][2]['content']
  71.             for i in range(1, len(self.queue)):
  72.                 if self.queue[i][2]['labels'] == targetlabels:
  73.                     newqueue[0][2]['content'].extend \
  74.                                    (self.queue[i][2]['content'])
  75.                 else:
  76.                     newqueue.append(self.queue[i]) 
  77.  
  78.             self.queue = newqueue
  79.  
  80.     def filterContents(self):
  81.         """ Combines utterances by eliminating repeated utterances and
  82.         utterances that are part of other utterances. """
  83.         if len(self.queue[0][2]['content']) > 1:
  84.             oldcontent = self.queue[0][2]['content']
  85.             newcontent = [oldcontent[0]]
  86.  
  87.             for i in range(1, len(oldcontent)):
  88.                 found = False
  89.                 for j in range(len(newcontent)):
  90.                     if oldcontent[i].find(newcontent[j]) != -1 \
  91.                         or newcontent[j].find(oldcontent[i]) != -1: 
  92.                         if len(oldcontent[i]) > len(newcontent[j]):
  93.                             newcontent[j] = oldcontent[i]
  94.                         found = True
  95.                         break
  96.  
  97.                 if not found:
  98.                     newcontent.append(oldcontent[i])
  99.  
  100.             self.queue[0][2]['content'] = newcontent
  101.  
  102.     def __len__(self):
  103.         """ Return the length of the queue """
  104.         return len(self.queue)
  105.  
  106.  
  107. class LiveRegionManager:
  108.     def __init__(self, script):
  109.         self._script = script
  110.         # message priority queue
  111.         self.msg_queue = PriorityQueue()
  112.  
  113.         # Message cache.  Used to store up to 9 previous messages so user can
  114.         # review if desired.
  115.         self.msg_cache = []
  116.  
  117.         # User overrides for politeness settings.
  118.         self._politenessOverrides = None
  119.         self._restoreOverrides = None
  120.  
  121.         # last live obj to be announced
  122.         self.lastliveobj = None
  123.  
  124.         # Used to track whether a user wants to monitor all live regions
  125.         # Not to be confused with the global Gecko.liveRegionsOn which 
  126.         # completely turns off live region support.  This one is based on
  127.         # a user control by changing politeness levels to LIVE_OFF or back
  128.         # to the bookmark or markup politeness value.
  129.         self.monitoring = True
  130.  
  131.         # Set up politeness level overrides and subscribe to bookmarks
  132.         # for load and save user events.
  133.         # We are initialized after bookmarks so call the load handler once
  134.         # to get initialized.
  135.         #
  136.         self.bookmarkLoadHandler()
  137.         script.bookmarks.addSaveObserver(self.bookmarkSaveHandler)
  138.         script.bookmarks.addLoadObserver(self.bookmarkLoadHandler)
  139.  
  140.     def reset(self):
  141.         # First we will purge our politeness override dictionary of LIVE_NONE
  142.         # objects that are not registered for this page
  143.         newpoliteness = {}
  144.         currenturi = self._script.bookmarks.getURIKey()
  145.         for key, value in self._politenessOverrides.iteritems():
  146.             if key[0] == currenturi or value != LIVE_NONE:
  147.                 newpoliteness[key] = value
  148.         self._politenessOverrides = newpoliteness
  149.  
  150.     def bookmarkSaveHandler(self):
  151.         """Bookmark save callback"""
  152.         self._script.bookmarks.saveBookmarksToDisk(self._politenessOverrides,
  153.                                                     filename='politeness')
  154.  
  155.     def bookmarkLoadHandler(self):
  156.         """Bookmark load callback"""
  157.         # readBookmarksFromDisk() returns None on error. Just initialize to an
  158.         # empty dictionary if this is the case.
  159.         self._politenessOverrides = \
  160.         self._script.bookmarks.readBookmarksFromDisk(filename='politeness') \
  161.         or {}
  162.  
  163.     def handleEvent(self, event):
  164.         """Main live region event handler"""
  165.         politeness = self._getLiveType(event.source)
  166.         if politeness == LIVE_OFF:
  167.             return
  168.         if politeness == LIVE_NONE:
  169.             # All the 'registered' LIVE_NONE objects will be set to off
  170.             # if not monitoring.  We will ignore LIVE_NONE objects that 
  171.             # arrive after the user switches off monitoring.
  172.             if not self.monitoring:
  173.                 return
  174.         elif politeness == LIVE_POLITE:
  175.             # Nothing to do for now
  176.             pass
  177.         elif politeness ==  LIVE_ASSERTIVE:
  178.             self.msg_queue.purgeByPriority(LIVE_POLITE)
  179.         elif politeness == LIVE_RUDE:
  180.             self.msg_queue.purgeByPriority(LIVE_ASSERTIVE)
  181.  
  182.         message = self._getMessage(event)
  183.         if message:
  184.             if len(self.msg_queue) == 0:
  185.                 gobject.timeout_add(100, self.pumpMessages)
  186.             self.msg_queue.enqueue(message, politeness, event.source)
  187.  
  188.     def pumpMessages(self):
  189.         """ Main gobject callback for live region support.  Handles both 
  190.         purging the message queue and outputting any queued messages that
  191.         were queued up in the handleEvent() method.
  192.         """
  193.         # If there are messages in the queue, we are monitoring, and we are not
  194.         # currently speaking then speak queued message.
  195.         # Note: Do all additional work within if statement to prevent
  196.         # it from being done for each event loop callback
  197.         # Note: isSpeaking() returns False way too early.  A strategy using
  198.         # a message length (in secs) could be used but don't forget many 
  199.         # parameters such as rate,expanded text and others must be considered.
  200.         if len(self.msg_queue) > 0 \
  201.                   and not speech.isSpeaking() \
  202.                   and time.time() - orca_state.lastInputEvent.time > 1:
  203.             # House cleaning on the message queue.  
  204.             # First we will purge the queue of old messages
  205.             self.msg_queue.purgeByKeepAlive()
  206.             # Next, we will filter the messages
  207.             self.msg_queue.clumpContents()
  208.             self.msg_queue.filterContents()
  209.             # Let's get our queued information
  210.             politeness, timestamp, message, obj = self.msg_queue.dequeue()
  211.             # Form output message.  No need to repeat labels and content.
  212.             # TODO: really needs to be tested in real life cases.  Perhaps
  213.             # a verbosity setting?
  214.             if message['labels'] == message['content']:
  215.                 utts = message['content']
  216.             else:
  217.                 utts = message['labels'] + message['content']
  218.             speech.speakUtterances(utts)
  219.  
  220.             # set the last live obj to be announced
  221.             self.lastliveobj = obj
  222.  
  223.             # cache our message
  224.             self._cacheMessage(utts)
  225.  
  226.         # We still want to maintain our queue if we are not monitoring
  227.         if not self.monitoring:
  228.             self.msg_queue.purgeByKeepAlive()
  229.  
  230.         # See you again soon, stay in event loop if we still have messages.
  231.         if len(self.msg_queue) > 0:
  232.             return True 
  233.         else:
  234.             return False
  235.         
  236.     def getLiveNoneObjects(self):
  237.         """Return the live objects that are registered and have a politeness
  238.         of LIVE_NONE. """
  239.         retval = []
  240.         currenturi = self._script.bookmarks.getURIKey()
  241.         for uri, objectid in self._politenessOverrides:
  242.             if uri == currenturi and isinstance(objectid, tuple):
  243.                 retval.append(self._script.bookmarks.pathToObj(objectid))
  244.         return retval
  245.  
  246.     def advancePoliteness(self, obj):
  247.         """Advance the politeness level of the given object"""
  248.         utterances = []
  249.         objectid = self._getObjectId(obj)
  250.         uri = self._script.bookmarks.getURIKey()
  251.  
  252.         try:
  253.             # The current priority is either a previous override or the
  254.             # live property.  If an exception is thrown, an override for 
  255.             # this object has never occurred and the object does not have
  256.             # live markup.  In either case, set the override to LIVE_NONE.
  257.             cur_priority = self._politenessOverrides[(uri, objectid)] 
  258.         except KeyError:
  259.             cur_priority = self._liveStringToType(obj)
  260.  
  261.         if cur_priority == LIVE_OFF or cur_priority == LIVE_NONE:
  262.             self._politenessOverrides[(uri, objectid)] = LIVE_POLITE
  263.             # Translators:  sets the live region politeness level to polite
  264.             #
  265.             utterances.append(_('setting live region to polite'))
  266.         elif cur_priority == LIVE_POLITE:
  267.             self._politenessOverrides[(uri, objectid)] = LIVE_ASSERTIVE
  268.             # Translators:  sets the live region politeness level to assertive
  269.             #
  270.             utterances.append(_('setting live region to assertive'))
  271.         elif cur_priority == LIVE_ASSERTIVE:
  272.             self._politenessOverrides[(uri, objectid)] = LIVE_RUDE
  273.             # Translators:  sets the live region politeness level to rude
  274.             #
  275.             utterances.append(_('setting live region to rude'))
  276.         elif cur_priority == LIVE_RUDE:
  277.             self._politenessOverrides[(uri, objectid)] = LIVE_OFF
  278.             # Translators:  sets the live region politeness level to off
  279.             #
  280.             utterances.append(_('setting live region to off'))
  281.  
  282.         speech.speakUtterances(utterances)
  283.  
  284.     def goLastLiveRegion(self):
  285.         """Move the caret to the last announced live region and speak the 
  286.         contents of that object"""
  287.         if self.lastliveobj:
  288.             self._script.setCaretPosition(self.lastliveobj, 0)
  289.             self._script.outlineAccessible(self.lastliveobj)
  290.             self._script.speakContents(self._script.getObjectContentsAtOffset(\
  291.                                        self.lastliveobj,0))
  292.  
  293.     def reviewLiveAnnouncement(self, msgnum):
  294.         """Speak the given number cached message"""
  295.         if msgnum > len(self.msg_cache):
  296.             # Tranlators: this tells the user that a cached message
  297.             # is not available.
  298.             #
  299.             speech.speak(_('no live message saved'))
  300.         else:
  301.             speech.speakUtterances(self.msg_cache[-msgnum])
  302.  
  303.     def setLivePolitenessOff(self):
  304.         """User toggle to set all live regions to LIVE_OFF or back to their
  305.         original politeness."""
  306.         # start at the document frame
  307.         docframe = self._script.getDocumentFrame()
  308.         # get the URI of the page.  It is used as a partial key.
  309.         uri = self._script.bookmarks.getURIKey()
  310.  
  311.         # The user is currently monitoring live regions but now wants to 
  312.         # change all live region politeness on page to LIVE_OFF
  313.         if self.monitoring:
  314.             # Translators: This lets the user know that all live regions
  315.             # have been turned off.
  316.             speech.speak(_("All live regions set to off"))
  317.  
  318.             self.msg_queue.clear()
  319.             
  320.             # First we'll save off a copy for quick restoration
  321.             self._restoreOverrides = copy.copy(self._politenessOverrides)
  322.  
  323.             # Set all politeness overrides to LIVE_OFF.
  324.             for override in self._politenessOverrides.keys():
  325.                 self._politenessOverrides[override] = LIVE_OFF
  326.  
  327.             # look through all the objects on the page and set/add to
  328.             # politeness overrides.  This only adds live regions with good
  329.             # markup.
  330.             matches = pyatspi.findAllDescendants(docframe, self.matchLiveRegion)
  331.             for match in matches:
  332.                 objectid = self._getObjectId(match)
  333.                 self._politenessOverrides[(uri, objectid)] = LIVE_OFF
  334.  
  335.             # Toggle our flag
  336.             self.monitoring = False
  337.  
  338.         # The user wants to restore politeness levels
  339.         else:
  340.             for key, value in self._restoreOverrides.iteritems():
  341.                 self._politenessOverrides[key] = value
  342.             # Translators: This lets the user know that all live regions
  343.             # have been restored to their original politeness level.
  344.             speech.speak(_("live regions politeness levels restored"))
  345.  
  346.             # Toggle our flag
  347.             self.monitoring = True  
  348.  
  349.     def outputLiveRegionDescription(self, obj):
  350.         """Used in conjuction with whereAmI to output description and 
  351.         politeness of the given live region object"""
  352.         objectid = self._getObjectId(obj)
  353.         uri = self._script.bookmarks.getURIKey()
  354.  
  355.         utterances = []
  356.  
  357.         # get the description if there is one.
  358.         for relation in obj.getRelationSet():
  359.             relationtype = relation.getRelationType()
  360.             if relationtype == pyatspi.RELATION_DESCRIBED_BY:
  361.                 targetobj = relation.getTarget(0)
  362.                 try:
  363.                     # We will add on descriptions if they don't duplicate
  364.                     # what's already in the object's description.
  365.                     # See http://bugzilla.gnome.org/show_bug.cgi?id=568467
  366.                     # for more information.
  367.                     #
  368.                     description = targetobj.queryText().getText(0, -1)
  369.                     if description.strip() != obj.description.strip():
  370.                         utterances.append(description)
  371.                 except NotImplemented:
  372.                     pass
  373.  
  374.         # get the politeness level as a string
  375.         try:
  376.             livepriority = self._politenessOverrides[(uri, objectid)]
  377.             liveprioritystr = self._liveTypeToString(livepriority)
  378.         except KeyError:
  379.             liveprioritystr = 'none'
  380.  
  381.         # We will only output useful information
  382.         # 
  383.         if utterances or liveprioritystr != 'none':
  384.             # Translators: output the politeness level
  385.             #
  386.             utterances.append(_('politeness level %s') %liveprioritystr)
  387.             speech.speakUtterances(utterances)
  388.  
  389.     def matchLiveRegion(self, obj):
  390.         """Predicate used to find a live region"""
  391.         attrs = self._getAttrDictionary(obj)
  392.         return 'container-live' in attrs
  393.  
  394.     def _getMessage(self, event):
  395.         """Gets the message associated with a given live event."""
  396.         attrs = self._getAttrDictionary(event.source)
  397.         content = [] 
  398.         labels = []
  399.         
  400.         # A message is divided into two parts: labels and content.  We
  401.         # will first try to get the content.  If there is None, 
  402.         # assume it is an invalid message and return None
  403.         if event.type.startswith('object:children-changed:add'):
  404.             # Get the text based on the atomic property
  405.             try:
  406.                 if attrs['container-atomic'] == 'true':
  407.                     # expand the source if atomic is true
  408.                     newcontent = self._script.expandEOCs(event.source)
  409.                 else:
  410.                     # expand the target if atomic is false
  411.                     newcontent = self._script.expandEOCs(event.any_data)
  412.             except (KeyError, TypeError):
  413.                 # expand the target if there is no ARIA markup
  414.                 newcontent = self._script.expandEOCs(event.any_data)
  415.  
  416.             # add our content to the returned message or return None if no
  417.             # content
  418.             if newcontent:
  419.                 content.append(newcontent)
  420.             else:
  421.                 return None
  422.  
  423.         else: #object:text-changed:insert
  424.             # Get a handle to the Text interface for the source.
  425.             # Serious problems if this fails.
  426.             #
  427.             try:
  428.                 sourceitext = event.source.queryText()
  429.             except NotImplementedError:
  430.                 return None
  431.  
  432.             # We found an embed character.  We can expect a children-changed
  433.             # event, which we will act on, so just return.
  434.             txt = sourceitext.getText(0, -1)
  435.             if txt.count(self._script.EMBEDDED_OBJECT_CHARACTER) > 0:
  436.                 return None
  437.  
  438.             # Get the text based on the atomic property
  439.             try:
  440.                 if attrs['container-atomic'] == 'true':
  441.                     newcontent = txt
  442.                 else:
  443.                     newcontent = txt[event.detail1:event.detail1+event.detail2]
  444.             except KeyError:
  445.                 newcontent = txt[event.detail1:event.detail1+event.detail2]
  446.  
  447.             # add our content to the returned message or return None if no
  448.             # content
  449.             if len(newcontent) > 0:
  450.                 content.append(newcontent)
  451.             else:
  452.                 return None
  453.  
  454.         # Get the labeling information now that we have good content.
  455.         labels = self._getLabelsAsUtterances(event.source)
  456.  
  457.         # instantly send out notify messages
  458.         if 'channel' in attrs and attrs['channel'] == 'notify':
  459.             utts = labels + content
  460.             speech.stop()
  461.             # Note: we would like to use a different ACSS for alerts.  This work
  462.             # should be done as part of bug #412656.
  463.             speech.speakUtterances(utts)
  464.             return None
  465.         else:
  466.             return {'content':content, 'labels':labels}
  467.  
  468.     def flushMessages(self):
  469.         self.msg_queue.clear()
  470.  
  471.     def _cacheMessage(self, utts):
  472.         """Cache a message in our cache list of length CACHE_SIZE"""
  473.         self.msg_cache.append(utts)
  474.         if len(self.msg_cache) > CACHE_SIZE:
  475.             self.msg_cache.pop(0)
  476.  
  477.     def _getLabelsAsUtterances(self, obj):
  478.         """Get the labels for a given object"""
  479.         # try the Gecko label getter first
  480.         uttstring = self._script.getDisplayedLabel(obj)
  481.         if uttstring:
  482.             return [uttstring.strip()]
  483.         # often we see a table cell.  I'll implement my own label getter
  484.         elif obj.getRole() == pyatspi.ROLE_TABLE_CELL \
  485.                            and obj.parent.childCount > 1:
  486.             # We will try the table interface first for it's parent
  487.             try:
  488.                 itable = obj.parent.queryTable()
  489.                 # I'm in a table, now what row are we in?  Look in the first 
  490.                 # columm of that row.
  491.                 #
  492.                 # Note: getRowHeader() fails for most markup.  We will use the
  493.                 # relation when the markup is good (when getRowHeader() works) 
  494.                 # so we won't see this code in those cases.  
  495.                 index = self._script.getCellIndex(obj)
  496.                 row = itable.getRowAtIndex(index)
  497.                 header = itable.getAccessibleAt(row, 0)
  498.                 # expand the header
  499.                 return [self._script.expandEOCs(header).strip()]
  500.             except NotImplementedError:
  501.                 pass
  502.  
  503.             # Last ditch effort is to see if our parent is a table row <tr> 
  504.             # element.
  505.             parentattrs = self._getAttrDictionary(obj.parent) 
  506.             if 'tag' in parentattrs and parentattrs['tag'] == 'TR':
  507.                 return [self._script.expandEOCs( \
  508.                                   obj.parent.getChildAtIndex(0)).strip()]
  509.  
  510.         # Sorry, no valid labels found
  511.         return []
  512.  
  513.     def _getLiveType(self, obj):
  514.         """Returns the live politeness setting for a given object. Also,
  515.         registers LIVE_NONE objects in politeness overrides when monitoring."""
  516.         objectid = self._getObjectId(obj)
  517.         uri = self._script.bookmarks.getURIKey()
  518.         if (uri, objectid) in self._politenessOverrides:
  519.             # look to see if there is a user politeness override
  520.             return self._politenessOverrides[(uri, objectid)]
  521.         else:
  522.             livetype = self._liveStringToType(obj)
  523.             # We'll save off a reference to LIVE_NONE if we are monitoring
  524.             # to give the user a chance to change the politeness level.  It
  525.             # is done here for performance sake (objectid, uri are expensive)
  526.             if livetype == LIVE_NONE and self.monitoring:
  527.                 self._politenessOverrides[(uri, objectid)] = livetype
  528.             return livetype
  529.  
  530.     def _getObjectId(self, obj):
  531.         """Returns the HTML 'id' or a path to the object is an HTML id is
  532.         unavailable"""
  533.         attrs = self._getAttrDictionary(obj)
  534.         if attrs is None:
  535.             return self._getPath(obj)
  536.         try:
  537.             return attrs['id']
  538.         except KeyError:
  539.             return self._getPath(obj)
  540.  
  541.     def _liveStringToType(self, obj, attributes=None):
  542.         """Returns the politeness enum for a given object"""
  543.         attrs = attributes or self._getAttrDictionary(obj)
  544.         try:
  545.             if attrs['container-live'] == 'off': 
  546.                 return LIVE_OFF
  547.             elif attrs['container-live'] == 'polite':  
  548.                 return LIVE_POLITE
  549.             elif attrs['container-live'] == 'assertive': 
  550.                 return LIVE_ASSERTIVE
  551.             elif attrs['container-live'] == 'rude': 
  552.                 return LIVE_RUDE
  553.             else: return LIVE_NONE
  554.         except KeyError:
  555.             return LIVE_NONE
  556.  
  557.     def _liveTypeToString(self, politeness):
  558.         """Returns the politeness level as a string given a politeness enum"""
  559.         if politeness == LIVE_OFF: 
  560.             return 'off'
  561.         elif politeness == LIVE_POLITE: 
  562.             return 'polite'
  563.         elif politeness == LIVE_ASSERTIVE: 
  564.             return 'assertive'
  565.         elif politeness == LIVE_RUDE: 
  566.             return 'rude'
  567.         elif politeness == LIVE_NONE: 
  568.             return 'none'
  569.         else: return 'unknown'
  570.  
  571.     def _getAttrDictionary(self, obj):
  572.         try:
  573.             return dict([attr.split(':', 1) for attr in obj.getAttributes()])
  574.         except:
  575.             return {}
  576.     
  577.     def _getPath(self, obj):
  578.         """ Returns, as a tuple of integers, the path from the given object 
  579.         to the document frame."""
  580.         docframe = self._script.getDocumentFrame()
  581.         path = []
  582.         while 1:
  583.             if obj.parent is None or obj == docframe:
  584.                 path.reverse()
  585.                 return tuple(path)
  586.             try:
  587.                 path.append(obj.getIndexInParent())
  588.             except Exception:
  589.                 raise LookupError
  590.             obj = obj.parent
  591.